<?php

namespace common\components;

use HttpException;
use Yii;
use yii\base\Component;
use yii\base\Exception;
use yii\base\InvalidConfigException;

/**
 * Class Weixin
 *
 * 微信组件
 *
 * 配置应用中的components
 * ~~~
 * 'weixin' => [
 *     'appid' => '', // 微信接口中的 appID
 *     'appSecret' => '', // 微信接口中的 appsecret
 *     'token' => '', //微信接口中的 token
 *     'weixinId' => '', //可选项 如果不设置 无法回复被动消息或客服消息
 * ]
 * ~~~
 *
 * 使用
 * ~~~
 * Yii::$app->weixin->getUserinfo($openid) //获取微信用户信息
 * ~~~
 *
 * @package common\components
 */
class Weixin extends Component
{

    /** 基础接口 access_token 获取地址 */
    const TOKEN_URL = 'https://api.weixin.qq.com/cgi-bin/token';

    /** 媒体上传接口地址 */
    const MEDIA_UPLOAD_URL = 'http://file.api.weixin.qq.com/cgi-bin/media/upload';

    /** 媒体获取接口地址 */
    const MEDIA_GET_URL = 'http://file.api.weixin.qq.com/cgi-bin/media/get';

    /** 菜单创建接口地址 */
    const MENU_CREATE_URL = 'https://api.weixin.qq.com/cgi-bin/menu/create';

    /** 餐单获取接口地址 */
    const MENU_GET_URL = 'https://api.weixin.qq.com/cgi-bin/menu/get';

    /** 餐单删除接口地址 */
    const MENU_DELETE_URL = 'https://api.weixin.qq.com/cgi-bin/menu/delete';

    /** 微信网页授权地址 */
    const AUTHORIZE_URL = 'https://open.weixin.qq.com/connect/oauth2/authorize';

    /** 微信网页授权 access_token 获取地址 */
    const WEB_ACCESS_TOKEN_URL = 'https://api.weixin.qq.com/sns/oauth2/access_token';

    /** 用户信息获取地址 */
    const USER_INFO_URL = 'https://api.weixin.qq.com/cgi-bin/user/info';

    /** 客服接口发送消息地址 */
    const MESSAGE_CUSTOM_SEND = 'https://api.weixin.qq.com/cgi-bin/message/custom/send';

    /** 微支付订单查询接口 */
    const ORDER_QUERY_URL = 'https://api.weixin.qq.com/pay/orderquery';

    /** 微支付发货通知接口 */
    const DELIVER_NOTIFY_URL = 'https://api.weixin.qq.com/pay/delivernotify';

    /** @var  string 公众号appID */
    public $appId;

    /** @var  string 公众号appsecret */
    public $appSecret;

    /** @var  string 接口配置中的token */
    public $token;

    /** @var  string 公众号账户名 */
    public $weixinId;

    public function init()
    {
        parent::init();

        if (!isset($this->appId, $this->appSecret, $this->token)) {
            throw new InvalidConfigException('appId, appSecret, token 不能为空');
        }
    }

    /**
     * 获取用户信息
     *
     * @param $openid
     * @return mixed
     */
    public function getUserInfo($openid)
    {
        return $this->request(
            self::USER_INFO_URL,
            array(
                'access_token' => $this->getAccessToken(),
                'openid' => $openid
            )
        );
    }

    /**
     * 上传媒体
     *
     * @param $file
     * @param $type
     * @return mixed
     */
    public function uploadMedia($file, $type)
    {
        if (!in_array($type, array('image', 'voice', 'video', 'thumb'))) {
            $type = 'image';
        }

        $result = $this->request(
            self::MEDIA_UPLOAD_URL,
            array(
                'access_token' => $this->getAccessToken(),
                'type' => $type,
            ),
            array(
                'media' => '@' . $file,
            )
        );
        return $result;
    }

    /**
     * 获取消息
     *
     * @param $data
     * @param array $query
     * @return array|bool
     */
    public function getMsg($data, $query = array())
    {
        if (!$this->_checkSignature($query)) {
            $request = Yii::$app->getRequest();
            Yii::warning(sprintf('微信信息验证失败，url:%s', $request->getUrl()));
            return false;
        }

        if ($data) {
            $result = simplexml_load_string($data);
            if ($result === false) {
                Yii::warning(sprintf('解析xml出错: %s', $data));
                return false;
            } else {
                return (array)$result;
            }
        } else {
            return isset($query['echostr']) ? $query['echostr'] : '';
        }
    }

    /**
     * 生成微信回复文本信息
     *
     * @param string $openid 用户的openid
     * @param string $text
     * @return string
     */
    public function makeTextMsg($openid, $text)
    {
        $createTime = time();
        return <<<EOT
<xml>
<ToUserName><![CDATA[{$openid}]]></ToUserName>
<FromUserName><![CDATA[{$this->weixinId}]]></FromUserName>
<CreateTime>{$createTime}</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[{$text}]]></Content>
</xml>
EOT;
    }

    /**
     * 生成微信回复的图文信息
     *
     * @param string $openid 用户的openid
     * @param array $articles
     * @return string
     */
    public function makeNewsMsg($openid, array $articles)
    {
        $createTime = time();
        $articleCount = count($articles);
        $xml = '';
        foreach ($articles as $article) {
            $xml .= "<item>\n"
                . "<Title><![CDATA[" . (isset($article['title']) ? $article['title'] : '') . "]]></Title>\n"
                . "<Description><![CDATA[" . (isset($article['description']) ? $article['description'] : '') . "]]></Description>\n"
                . "<PicUrl><![CDATA[" . (isset($article['picUrl']) ? $article['picUrl'] : '') . "]]></PicUrl>\n"
                . "<Url><![CDATA[" . (isset($article['url']) ? $article['url'] : '') . "]]></Url>\n"
                . "</item>\n";
        }
        return <<<EOT
<xml>
<ToUserName><![CDATA[{$openid}]]></ToUserName>
<FromUserName><![CDATA[{$this->weixinId}]]></FromUserName>
<CreateTime>{$createTime}</CreateTime>
<MsgType><![CDATA[news]]></MsgType>
<ArticleCount>{$articleCount}</ArticleCount>
<Articles>{$xml}</Articles>
</xml>
EOT;
    }

    /**
     * 生成客服文本信息
     *
     * @param string $openid 用户的openid
     * @param $text
     * @return string
     */
    public function makeCustomTextMsg($openid, $text)
    {
        $text = addslashes($text);
        return <<<EOT
{
	"touser": "{$openid}",
	"msgtype": "text",
	"text": {
		"content": "{$text}"
	}
}
EOT;
    }

    /**
     * 生成客服图片消息
     *
     * @param string $openid 用户的openid
     * @param $mediaId
     * @return string
     */
    public function makeCustomImageMsg($openid, $mediaId)
    {
        return <<<EOT
{
	"touser": "{$openid}",
	"msgtype": "image",
	"image": {
		"media_id": "{$mediaId}"
	}
}
EOT;
    }

    /**
     * 生成客服语音信息
     *
     * @param string $openid 用户的openid
     * @param $mediaId
     * @return string
     */
    public function makeCustomVoiceMsg($openid, $mediaId)
    {
        return <<<EOT
{
	"touser": "{$openid}",
	"msgtype": "voice",
	"voice": {
		"media_id": "{$mediaId}"
	}
}
EOT;
    }

    /**
     * 生成客服视频信息
     *
     * @param string $openid 用户的openid
     * @param $mediaId
     * @param string $title
     * @param string $description
     * @return string
     */
    public function makeCustomVideoMsg($openid, $mediaId, $title = '', $description = '')
    {
        $title = addslashes($title);
        $description = addslashes($description);
        return <<<EOT
{
	"touser": "{$openid}",
	"msgtype": "video",
	"video": {
		"media_id": "{$mediaId}",
		"title": "{$title}",
		"description" "{$description}"
	}
}
EOT;
    }

    /**
     * 生成客服图文信息
     *
     * @param string $openid 用户的openid
     * @param $articles
     * @return string
     */
    public function makeCustomNewsMsg($openid, $articles)
    {
        $output = array();
        foreach ($articles as $article) {
            $str = array();
            if (isset($article['title'])) {
                $str[] = '"title":"' . addslashes($article['title']) . '"';
            }
            if (isset($article['description'])) {
                $str[] = '"description":"' . addslashes($article['description']) . '"';
            }
            if (isset($article['url'])) {
                $str[] = '"url":"' . $article['url'] . '"';
            }
            if (isset($article['picUrl'])) {
                $str[] = '"picurl":"' . $article['picUrl'] . '"';
            }
            $output[] = '{' . implode(',', $str) . '}';
        }
        $output = implode(',', $output);
        return <<<EOT
{
	"touser":"{$openid}",
	"msgtype":"news",
	"news":{
		"articles": [
			{$output}
		]
	}
}
EOT;

    }

    /**
     * 发送客服信息
     *
     * @param $customMsg
     * @throws WeixinException
     */
    public function messageCustomSend($customMsg)
    {
        $this->request(
            self::MESSAGE_CUSTOM_SEND,
            array(
                'access_token' => $this->getAccessToken(),
            ),
            $customMsg
        );
    }

    /**
     * 获取网页授权url
     *
     * @param string $redirectUri 跳转地址
     * @param string $scope
     * @param null $state
     * @return string
     */
    public function getAuthorizeUrl($redirectUri, $scope = 'snsapi_userinfo', $state = null)
    {
        $params = array(
            'appid' => $this->appId,
            'redirect_uri' => $redirectUri,
            'response_type' => 'code',
            'scope' => $scope,
        );

        if ($state) {
            $params['state'] = $state;
        }
        return self::AUTHORIZE_URL . '?' . http_build_query($params, null, '&') . '#wechat_redirect';
    }

    /**
     * 获取网页授权的 access_token
     *
     * @param string $code 授权后跳转带的code参数
     * @return mixed
     * @throws WeixinException
     */
    public function getAuthorizeAccessToken($code)
    {
        return $this->request(
            self::WEB_ACCESS_TOKEN_URL,
            array(
                'appid' => $this->appId,
                'secret' => $this->appSecret,
                'code' => $code,
                'grant_type' => 'authorization_code',
            )
        );
    }

    /**
     * 获取媒体
     *
     * @param $mediaId
     * @return mixed
     */
    public function getMedia($mediaId)
    {
        return $this->request(
            self::MEDIA_GET_URL,
            array(
                'access_token' => $this->getAccessToken(),
                'media_id' => $mediaId,
            )
        );
    }

    /**
     * 创建餐单
     *
     * @param $menu
     * @return mixed
     */
    public function createMenu($menu)
    {
        return $this->request(
            self::MENU_CREATE_URL,
            array(
                'access_token' => $this->getAccessToken(),
            ),
            is_array($menu) ? json_encode($menu) : $menu
        );
    }

    /**
     * 获取餐单
     *
     * @param bool $raw
     * @return mixed
     */
    public function getMenu($raw = false)
    {
        return $this->request(
            self::MENU_GET_URL,
            array(
                'access_token' => $this->getAccessToken(),
            ),
            null,
            $raw
        );
    }

    /**
     * 删除餐单
     *
     * @return mixed
     */
    public function deleteMenu()
    {
        return $this->request(
            self::MENU_DELETE_URL,
            array(
                'access_token' => $this->getAccessToken(),
            )
        );
    }

    /**
     * 订单查询
     *
     *
     * {
     * "appid" : "wwwwb4f85f3a797777",
     * "package" : "out_trade_no=11122&partner=1900090055&sign=4e8d0df3da0c3d0df38f",
     * "timestamp" : "1369745073",
     * "app_signature" : "53cca9d47b883bd4a5c85a9300df3da0cb48565c",
     * "sign_method" : "sha1"
     * }
     *
     *
     * @param mixed $data json数据
     * @return mixed
     */
    public function orderQuery($data)
    {
        return $this->request(
            self::ORDER_QUERY_URL,
            array(
                'access_token' => $this->getAccessToken(),
            ),
            is_array($data) ? json_encode($data) : $data
        );
    }

    /**
     * 通知发货
     *
     *
     * {
     * "appid" : "wwwwb4f85f3a797777",
     * "openid" : "oX99MDgNcgwnz3zFN3DNmo8uwa-w",
     * "transid" : "111112222233333",
     * "out_trade_no" : "555666uuu",
     * "deliver_timestamp" : "1369745073",
     * "deliver_status" : "1",
     * "deliver_msg" : "ok",
     * "app_signature" : "53cca9d47b883bd4a5c85a9300df3da0cb48565c",
     * "sign_method" : "sha1"
     * }
     * @param $data
     * @return mixed
     */
    public function deliverNotify($data)
    {
        return $this->request(
            self::DELIVER_NOTIFY_URL,
            array(
                'access_token' => $this->getAccessToken(),

            ),
            is_array($data) ? json_encode($data) : $data
        );
    }

    /**
     * 获取访问token
     * 该方法用的缓存组件 保存获取到的 access_token
     *
     * @return string
     */
    public function getAccessToken()
    {
        $cache = Yii::$app->getCache();
        if (($accessToken = $cache->get('_weixin_access_token')) === false || $accessToken['expires'] < time()) {
            $result = $this->request(
                self::TOKEN_URL,
                array(
                    'grant_type' => 'client_credential',
                    'appid' => $this->appId,
                    'secret' => $this->appSecret,
                )
            );
            $accessToken = array(
                'value' => $result['access_token'],
                'expires' => time() + $result['expires_in'],
            );
            $cache->set('_weixin_access_token', $accessToken);
        }

        return $accessToken['value'];
    }

    /**
     * 请求数据
     *
     * @param string $url
     * @param mixed $get
     * @param mixed $post
     * @param bool $raw
     * @return mixed
     * @throws Exception
     * @throws HttpException
     * @throws WeixinException
     */
    public function request($url, $get = null, $post = null, $raw = false)
    {
        if (is_array($get)) {
            $url .= '?' . http_build_query($get, '', '&');
        }

        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HEADER, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

        if ($post !== null) {
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
        }

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

        curl_close($ch);

        if ($response === false) {
            throw new Exception(curl_error($ch));
        }

        if ($httpCode != 200) {
            throw new HttpException($httpCode);
        }

        if ($raw) {
            return $response;
        }

        $result = @json_decode($response, true);
        if (isset($result['errcode']) && $result['errcode'] != 0) {
            throw new WeixinException($result['errcode'] . ' ' . $result['errmsg'], $result['errcode']);
        } else {
            return $result;
        }
    }

    /**
     * 验证微信消息签名
     *
     * @param array $query
     * @return bool
     */
    private function _checkSignature($query = [])
    {
        if (empty($query["signature"]) || empty($query["timestamp"]) || empty($query["nonce"])) {
            return false;
        }

        $signature = $query["signature"];
        $timestamp = $query["timestamp"];
        $nonce = $query["nonce"];
        $token = $this->token;
        $tmpArr = array($token, $timestamp, $nonce);
        sort($tmpArr, SORT_STRING);
        $tmpStr = implode($tmpArr);
        $tmpStr = sha1($tmpStr);
        if ($tmpStr === $signature) {
            return true;
        } else {
            return false;
        }
    }
}

class WeixinException extends Exception
{
}
